
package com.hlkj.pay.app.merchant.ext.impl.helipay.util;
/*
 * Hlpay-Plus aggregate payment system.
 * Copyright (c) 2024-2025 Hlpay Team Copyright has the right of final interpretation.
 */

import com.hlkj.framework.common.util.json.JsonUtils;
import com.hlkj.pay.app.merchant.ext.impl.helipay.dto.HeliPayConfig;
import com.hlkj.pay.app.merchant.ext.impl.helipay.dto.SignAndEncryptedContent;
import com.hlkj.pay.app.merchant.ext.impl.helipay.dto.req.HeliPayAccessRes;
import com.hlkj.pay.util.CommonSignUtil;
import com.hlkj.pay.util.SM2Utils;
import com.hlkj.pay.util.SM4Utils;
import net.dreamlu.mica.core.utils.StringPool;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM4Engine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.util.Enumeration;
import java.util.Map;
import java.util.Objects;

/**
 * @author HlpayTeam
 * @date 2024/12/12 16:28
 */
public class HeliPaySignUtil {

    private static final String SPLIT = "&";

    private static Logger logger = LoggerFactory.getLogger(HeliPaySignUtil.class);

    public static SignAndEncryptedContent encryptAndSign(HeliPayConfig heliPayConfig, String jsonParams) throws Exception {
        logger.info("业务请求参数jsonParams:" + jsonParams);
        if (StringUtils.isBlank(jsonParams)) {
            throw new IllegalArgumentException("str 不能为空");
        }
        SignatureType signType = SignatureType.SM3WITHSM2;
        String encryptedStr;
        String sources;
        String encryptionKey = null;
        String sign;
        String sm4Key = SM4Utils.generateRandomKey();
        encryptedStr = SM4Utils.encryptBase64(jsonParams, sm4Key);
        encryptionKey = SM2Utils.encryptToBase64(sm4Key, CommonSignUtil.getX509Certificate(heliPayConfig.getPlatPublicKeyPath()));
        logger.info("merchantNo:" + heliPayConfig.getMerchantNo() + ",明文sm4Key:" + sm4Key + ",加密encryptionKey:" + encryptionKey);
        sources = encryptedStr + SPLIT + heliPayConfig.getMerchantNo();
        // String keyContent = new
        // String(Files.readAllBytes(Paths.get(heliPayConfig.getPrivateKeyPath())));
        String privateKey = getPrivateKeyStr(heliPayConfig.getPrivateKeyPath(), heliPayConfig.getSm2Key());
        ;
        sign = SM2Utils.sign(sources, privateKey);
        logger.info("encryptedStr:" + encryptedStr);
        logger.info("sources:" + sources);
        logger.info("signType:" + signType);
        logger.info("sign:" + sign);
        return new SignAndEncryptedContent(sign, encryptedStr, signType, encryptionKey);
    }

    /**
     * 延迟分账签名
     */
    public static SignAndEncryptedContent fundSign(HeliPayConfig heliPayConfig, String jsonParams) throws Exception {
        logger.info("业务请求参数jsonParams:" + jsonParams);
        if (StringUtils.isBlank(jsonParams)) {
            throw new IllegalArgumentException("str 不能为空");
        }
        SignatureType signType = SignatureType.SM3WITHSM2;
        String sm4Key = SM4Utils.generateRandomKey();
        String data = SM4Utils.encryptBase64(jsonParams, sm4Key);
        String encryptionKey = SM2Utils.encryptToBase64(sm4Key, CommonSignUtil.getX509Certificate(heliPayConfig.getPlatPublicKeyPath()));
        logger.info("merchantNo:" + heliPayConfig.getMerchantNo() + ",明文sm4Key:" + sm4Key + ",加密encryptionKey:" + encryptionKey);
        String privateKey = getPrivateKeyStr(heliPayConfig.getPrivateKeyPath(), heliPayConfig.getSm2Key());

        String sign = SM2Utils.sign(data, privateKey);
        logger.info("encryptedStr:" + encryptionKey);
        logger.info("source:" + data);
        logger.info("signType:" + signType);
        logger.info("sign:" + sign);
        return new SignAndEncryptedContent(sign, data, signType, encryptionKey);
    }
    /**
     * 获取私钥字符串
     *
     * @param sm2FilePath
     * @param sm2FilePwd
     * @return
     * @throws IOException
     */
    public static String getPrivateKeyStr(String sm2FilePath, String sm2FilePwd) throws Exception {
        Objects.requireNonNull(sm2FilePath, "sm2FilePath required");
        Objects.requireNonNull(sm2FilePwd, "sm2FilePwd required");
        // 新证书是pfx格式
        if (sm2FilePath.endsWith(".pfx")) {
            return getPrivateKeyForPfx(sm2FilePath, sm2FilePwd);
        }
        else {
            return getPrivateKeyForSm2(sm2FilePath, sm2FilePwd);
        }
    }

    @Deprecated
    private static String getPrivateKeyForSm2(String sm2FilePath, String sm2FilePwd) throws Exception {
        byte[] data = Files.readAllBytes(Paths.get(sm2FilePath));
        try {
            PKCS12_SM2 p12 = new PKCS12_SM2(data);
            return p12.getPrivateKey(sm2FilePwd);
        }
        catch (Exception e) {
            logger.error("getPrivateKeyStr", e);
            throw new IllegalArgumentException(e.getMessage(), e);
        }
    }
    public static String doSignStrAppend(Map<String, String> map) {
        StringBuffer sb = new StringBuffer();
        for (String key : map.keySet()) {
            // do sign append
            String value = map.get(key);
            if (value == null) {
                map.put(key, "");
            }
        }
        for (String key : map.keySet()) {
            // do sign append
            String value = map.get(key);
            value = (value == null ? "" : value);
            sb.append(StringPool.AMPERSAND).append(value);
        }
        return sb.toString();
    }
    private static String getPrivateKeyForPfx(String pfxFilePath, String pfxFilePwd) throws Exception {
        try (FileInputStream fis = new FileInputStream(pfxFilePath);) {
            KeyStore ks = KeyStore.getInstance("PKCS12", BouncyCastleProvider.PROVIDER_NAME);

            // If the keystore password is empty(""), then we have to set
            // to null, otherwise it won't work!!!
            char[] nPassword = null;
            if ((pfxFilePwd == null) || pfxFilePwd.trim().equals("")) {
                nPassword = null;
            }
            else {
                nPassword = pfxFilePwd.toCharArray();
            }
            ks.load(fis, nPassword);
            fis.close();
            Enumeration enumas = ks.aliases();
            String keyAlias = null;
            if (enumas.hasMoreElements())// we are readin just one certificate.
            {
                keyAlias = (String) enumas.nextElement();
            }
            PrivateKey prikey = (PrivateKey) ks.getKey(keyAlias, nPassword);
            return org.apache.commons.codec.binary.Base64.encodeBase64String(prikey.getEncoded());
        }
        catch (Exception e) {
            logger.error("getPrivateKeyStr", e);
            throw new IllegalArgumentException(e.getMessage(), e);
        }
    }

    // public static String verificationAndDecrypt(HeliPayConfig heliPayConfig, HeliPayAccessRes heliPayAccessRes) throws IOException {
    //     String encrypted = heliPayAccessRes.getData();
    //     String sign = heliPayAccessRes.getSign();
    //     SignatureType signType = heliPayAccessRes.getSignType();
    //     String encryptionKey = heliPayAccessRes.getEncryptionKey();
    //     // 使用合利宝公钥进行验签
    //     boolean verify = SM2Utils.verify(encrypted, sign, CommonSignUtil.getX509Certificate(heliPayConfig.getPlatPublicKeyPath()));
    //     if (!verify) {
    //         throw new RuntimeException("通道正常响应,我方验签失败了");
    //     }
    //     try {
    //         String keyContent = new String(Files.readAllBytes(Paths.get(heliPayConfig.getPrivateKeyPath())));
    //         String privateKey = RSAUtil.removePemHeadersAndFooters(keyContent);
    //         // 使用商户私钥进行解密
    //         String sm4Key = SM2Utils.decryptBase64Message(encryptionKey, privateKey);
    //         // sm4进行解密
    //         String resBody = SM4Utils.decryptBase64(encrypted, sm4Key);
    //         logger.info("解密后的数据：" + resBody);
    //         return resBody;
    //     }
    //     catch (Exception e) {
    //         logger.info("加密串：" + encrypted, e);
    //         logger.info("解密秘钥：" + encryptionKey);
    //         throw new RuntimeException("通道正常响应,我方解密失败了");
    //     }
    // }

    public static String verificationAndDecrypt(HeliPayConfig heliPayConfig, HeliPayAccessRes heliPayAccessRes) {
        String encrypted = heliPayAccessRes.getData();
        String sign = heliPayAccessRes.getSign();
        SignatureType signType = heliPayAccessRes.getSignType();
        String encryptionKey = heliPayAccessRes.getEncryptionKey();
        // 使用合利宝公钥进行验签
        boolean verify = SM2Utils.verify(heliPayAccessRes.getData(), heliPayAccessRes.getSign(), CommonSignUtil.getX509Certificate(heliPayConfig.getPlatPublicKeyPath()));
        if (!verify) {
            throw new RuntimeException("通道正常响应,我方验签失败了");
        }
        try {
            String merchantPrivateKeyStr = getPrivateKeyStr(heliPayConfig.getPrivateKeyPath(), heliPayConfig.getSm2Key());
            // 使用商户私钥进行解密
            String sm4Key = SM2Utils.decryptBase64Message(encryptionKey, merchantPrivateKeyStr);
            // sm4进行解密
            String resBody = SM4Utils.decryptBase64(encrypted, sm4Key);
            logger.info("解密后的数据：" + resBody);
            return resBody;
        }
        catch (Exception e) {
            logger.info("加密串：" + encrypted, e);
            logger.info("解密秘钥：" + encryptionKey);
            throw new RuntimeException("通道正常响应,我方解密失败了");
        }
    }

    public static void main(String[] args) {
        String res="{\"success\":true,\"code\":\"0000\",\"data\":\"x5I5CnSGv13NkC+7eVS/2DjJrahm0EbyiSVEJnBL2SdyKq6esJd5Ipbe9ODa+33zjgSsQO61+Xu9Vvc7Ebh5ItIzlM8Ulhv8Oho9/Fs5I2a5FAy2rvuYKAw5i+8qmkSNPWDZvoVUDTAK/+d91R+vlLeptEadSxIFC3dGuz2Vp5w=\",\"sign\":\"MEUCIQDWZdQjaZ1GK2UgDpYmbudKYMzGYeu4+0fLsxMjlHTM8QIgZfhrg7+89Nh4i0NpbLFipJyqX1XBSb2CWB+fMSDOatA=\",\"signType\":\"SM3WITHSM2\",\"hostname\":\"shelipay_trx23\",\"interfaceName\":\"register\",\"encryptionKey\":\"BEDE5aTRmguFDb62Y8HOrqQ+9n3XmHrkIF/nZxJ24owDY6JcDRw5M0uypE2WNgx+oHsNM0Lm8Ln7yKRCvasvwRt/qnZ+4YVLw2TJgOOqmMPps8WGPW5QzWXEo8dhT8gzUOW/3Eu6x7V/Z6biDbbFwiE=\",\"timestamp\":\"2024-12-29_09:20:26.931\"}";
        String config="{\"merchantNo\":\"C1808685630\",\"serialNo\":\"2409030121763411\",\"sm2Key\":\"123456\",\"privateKeyPath\":\"E:/work/local_file/secret/20241227/C1808685630_2409030121763411_privateKey.pfx\",\"publicKeyPath\":\"E:/work/local_file/secret/20241227/C1808685630_2409030121763411_publicKey.cer\",\"platPublicKeyPath\":\"E:/work/local_file/secret/20241227/helipayPublicKey.cer\",\"feePurpose\":\"\",\"channelName\":\"\",\"applyType\":\"\"}";

        System.out.println(verificationAndDecrypt(JsonUtils.parseObject(config,HeliPayConfig.class),JsonUtils.parseObject(res,HeliPayAccessRes.class)));
    }

    public static final String ENCODE = "UTF-8";
    public static String disguiseMD5(String message) {
        if (null == message) {
            return null;
        }
        return disguiseMD5(message, ENCODE);
    }
    public static String disguiseMD5(String message, String encoding) {

        if (null == message || null == encoding) {

            return null;
        }

        message = message.trim();
        byte value[];
        try {
            value = message.getBytes(encoding);
        } catch (UnsupportedEncodingException e) {
            value = message.getBytes();
        }
        MessageDigest md = null;
        try {
            md = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
        return toHex(md.digest(value));
    }
    public static String toHex(byte input[]) {
        if (input == null)
            return null;
        StringBuffer output = new StringBuffer(input.length * 2);
        for (int i = 0; i < input.length; i++) {
            int current = input[i] & 0xff;
            if (current < 16)
                output.append("0");
            output.append(Integer.toString(current, 16));
        }

        return output.toString();
    }
    @Deprecated
    private static class PKCS12_SM2 implements ASN1Encodable, PKCSObjectIdentifiers {

        private ASN1Sequence privateInfo = null;

        public PKCS12_SM2() {
        }

        public PKCS12_SM2(byte[] data) throws Exception {
            load(data);
        }

        public void load(byte[] data) throws Exception {
            ASN1Sequence seq = getDERSequenceFrom(data);
            parseSM2(seq);
        }

        public void parseSM2(ASN1Sequence seq) throws Exception {
            parseSM2Certs((ASN1Sequence) seq.getObjectAt(1), (ASN1Sequence) seq.getObjectAt(2));
        }

        private void parseSM2Certs(ASN1Sequence privateInfo, ASN1Sequence publicInfo) {
            this.privateInfo = privateInfo;
        }

        public static ASN1Sequence getDERSequenceFrom(byte[] encoding) throws Exception {
            if (isDERSequence(encoding)) {
                return ASN1Sequence.getInstance(encoding);
            }
            if (isBERSequence(encoding)) {
                return ASN1Sequence.getInstance(encoding);
            }
            byte[] data;
            try {
                data = Base64.decode(encoding);
            }
            catch (Exception e) {
                throw new Exception("encoding required base64 encoding", e);
            }
            return ASN1Sequence.getInstance(data);
        }

        public static boolean isBERSequence(byte[] encoding) throws Exception {
            if (encoding == null) {
                throw new Exception("encoding should not be null");
            }
            if (encoding.length < 4) {
                throw new Exception("encoding length less than 4");
            }
            if (encoding[0] != 48) {
                return false;
            }
            int offset = 1;
            int length = encoding[(offset++)] & 0xFF;
            if (length != 128) {
                return false;
            }
            return (encoding[(encoding.length - 1)] == 0) && (encoding[(encoding.length - 2)] == 0);
        }

        public static boolean isDERSequence(byte[] encoding) throws Exception {
            if (encoding == null) {
                throw new Exception("encoding should not be null");
            }
            if (encoding.length < 2) {
                throw new Exception("encoding length less than 4");
            }
            if (encoding[0] != 48) {
                return false;
            }
            int offset = 1;
            int length = encoding[(offset++)] & 0xFF;
            if (length == 128) {
                return false;
            }
            if (length > 127) {
                int dLength = length & 0x7F;
                if (dLength > 4) {
                    return false;
                }
                length = 0;
                int next = 0;
                for (int i = 0; i < dLength; i++) {
                    next = encoding[(offset++)] & 0xFF;
                    length = (length << 8) + next;
                }
                if (length < 0) {
                    return false;
                }
            }
            return encoding.length == offset + length;
        }

        @Deprecated
        private String getPrivateKey(String password) throws Exception {
            return decrypt(password);
        }

        @Deprecated
        private String decrypt(String password) throws Exception {
            if (password == null) {
                throw new Exception("SM2File password should not be null");
            }
            if (this.privateInfo == null) {
                throw new Exception("SM2File invalid : privateInfo=null");
            }
            ASN1OctetString priOctString = (ASN1OctetString) this.privateInfo.getObjectAt(2);
            byte[] encryptedData;
            try {
                encryptedData = priOctString.getOctets();
            }
            catch (Exception e) {
                throw new Exception("SM2File decoding failure", e);
            }
            byte[] dBytes = SM4DecryptDBytes(password, encryptedData);

            return Hex.toHexString(dBytes).substring(0, 64);
        }

        private byte[] SM4DecryptDBytes(String password, byte[] encryptedData) throws Exception {
            if ((password == null) || (password.length() == 0)) {
                throw new Exception("SM2File password should not be null");
            }
            byte[] passwordBytes;
            try {
                passwordBytes = password.getBytes("UTF8");
            }
            catch (UnsupportedEncodingException e) {
                throw new Exception("SM2File password decoding failure", e);
            }
            if ((encryptedData == null) || (encryptedData.length == 0)) {
                throw new Exception("SM2File encryptedData should not be null");
            }
            if ((encryptedData.length < 32) || (encryptedData.length > 64)) {
                throw new Exception("SM2File EncryptedData required length in [32-64] ");
            }
            byte[] encoding = null;
            if ((encryptedData.length == 32) || (encryptedData.length == 48)) {
                encoding = encryptedData;
            }
            else {
                try {
                    encoding = Base64.decode(encryptedData);
                }
                catch (Exception e) {
                    throw new Exception("SM2File EncryptedData required base64 ");
                }
            }
            byte[] iv;
            byte[] sm4;
            try {
                byte[] hash = KDF(passwordBytes);
                iv = new byte[16];
                System.arraycopy(hash, 0, iv, 0, 16);
                sm4 = new byte[16];
                System.arraycopy(hash, 16, sm4, 0, 16);
            }
            catch (Exception e) {
                throw new Exception("SM2File KDF failure", e);
            }
            try {
                PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new SM4Engine()), new PKCS7Padding());
                ParametersWithIV params = new ParametersWithIV(new KeyParameter(sm4), iv);
                cipher.init(false, params);

                int outLength = cipher.getOutputSize(encoding.length);
                byte[] out = new byte[outLength];
                int dataLength = cipher.processBytes(encoding, 0, encoding.length, out, 0);
                int lastLength = cipher.doFinal(out, dataLength);
                int realLength = dataLength + lastLength;
                byte[] dBytes = null;
                if (realLength < outLength) {
                    dBytes = new byte[realLength];
                    System.arraycopy(out, 0, dBytes, 0, realLength);
                }
                return out;
            }
            catch (DataLengthException e) {
                throw new Exception("SM2File SM2PrivateKey decrypt failure with IllegalDataLength", e);
            }
            catch (IllegalArgumentException e) {
                throw new Exception("SM2File SM2PrivateKey decrypt failure with IllegalArgument", e);
            }
            catch (IllegalStateException e) {
                throw new Exception("SM2File SM2PrivateKey decrypt failure with IllegalState", e);
            }
            catch (InvalidCipherTextException e) {
                throw new Exception("SM2File SM2PrivateKey decrypt failure with InvalidCipherText", e);
            }
            catch (Exception e) {
                throw new Exception("SM2File SM2PrivateKey decrypt failure", e);
            }
        }

        private byte[] KDF(byte[] z) {
            byte[] ct = { 0, 0, 0, 1 };
            SM3Digest sm3 = new SM3Digest();
            sm3.update(z, 0, z.length);
            sm3.update(ct, 0, ct.length);
            byte[] hash = new byte[32];
            sm3.doFinal(hash, 0);
            return hash;
        }

        @Override
        public ASN1Primitive toASN1Primitive() {
            return null;
        }

    }
}
